package com.wva.video.service.impl;

import com.wva.minio.utils.MinIOUtils;
import com.wva.video.constant.UploadConst;
import com.wva.video.domain.dto.CheckChunksDTO;
import com.wva.video.domain.dto.SetChunkMd5DTO;
import com.wva.video.domain.vo.CheckChunksVO;
import com.wva.common.enums.ObjectName;
import com.wva.video.enums.RedisKey;
import com.wva.video.service.VideoUploadService;
import io.minio.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.time.ZonedDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @Author: hong
 * @Date: 2025/2/8 23:03:47
 */
@Service
@Slf4j
public class VideoUploadServiceImpl implements VideoUploadService {
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Autowired
    MinIOUtils minIOUtils;
    // @Autowired
    // VideoProcessService videoProcessService;

    @Override
    public void setChunkMd5(SetChunkMd5DTO setChunkMd5DTO) {
        setChunkMd5(
                setChunkMd5DTO.getFileMd5(),
                setChunkMd5DTO.getChunkId(),
                setChunkMd5DTO.getChunkMd5()
        );
    }

    @Async
    @Override
    public void mergeChunks(String fileMd5) throws Exception {
        //计算耗时
        long start = System.currentTimeMillis();

        List<Integer> ids = getChunkSortedIdsByFileMd5(fileMd5);

        //将视频分块一个一个添加到集合等待合并
        List<ComposeSource> sourceObjectList = new ArrayList<ComposeSource>();
        for (Integer id : ids) {
            sourceObjectList.add(
                    ComposeSource.builder()
                            .bucket(ObjectName.VIDEO_PART.getBucket().getBucketName())
                            .object(ObjectName.VIDEO_PART.format(fileMd5, id))
                            .build()
            );
        }

        //组合分块
        ObjectWriteResponse objectWriteResponse = minIOUtils.getClient().composeObject(
                ComposeObjectArgs.builder()
                        .bucket(ObjectName.TEMP.getBucket().getBucketName())
                        .object(ObjectName.TEMP.format(fileMd5))
                        .sources(sourceObjectList)
                        .build()
        );

        //合并完毕
        stringRedisTemplate.opsForValue().set(RedisKey.TEMP_VIDEO.format(fileMd5), "1");

        //TODO 校验文件完整性
        String etag = objectWriteResponse.etag();

        //删除视频分片文件
        removeVideoParts(fileMd5, ids);

        //删除redis内的分片
        stringRedisTemplate.delete(RedisKey.CHUNKS_MD5.format(fileMd5));
        stringRedisTemplate.opsForZSet().remove(RedisKey.CHUNKS_DEADLINE.format(), fileMd5);

        //记录合并日志
        long time = (System.currentTimeMillis() - start) / 1000;
        log.info("合并视频{}成功，总耗时{}s", fileMd5, time);
    }

    /**
     * 从redis获取分块id并排序好
     * @param fileMd5
     * @return
     */
    public List<Integer> getChunkSortedIdsByFileMd5(String fileMd5) {
        Map<Object, Object> chunks = getAlreadyUploadChunks(fileMd5);
        return chunks.keySet().stream().map(object -> Integer.valueOf((String) object)).sorted()
                .collect(Collectors.toList());
    }

    /**
     * 删除上传过后的分片文件
     * @param fileMd5
     * @param ids
     * @throws Exception
     */
    public void removeVideoParts(String fileMd5, List<Integer> ids) throws Exception {
        List<DeleteObject> objects = new LinkedList<>();
        for (Integer id : ids) {
            objects.add(new DeleteObject(ObjectName.VIDEO_PART.format(fileMd5, id)));
        }
        Iterable<Result<DeleteError>> results = minIOUtils.getClient().removeObjects(
                RemoveObjectsArgs.builder()
                        .bucket(ObjectName.VIDEO_PART.getBucket().getBucketName())
                        .objects(objects)
                        .build()
        );
        for (Result<DeleteError> result : results) {
            DeleteError error = result.get();
            log.error("删除对象{}出错，信息：{}", error.bucketName(), error.message());
        }
    }


    public void removeVideoPartsByMd5(String fileMd5) throws Exception {
        //从minIO删除分块文件
        List<Integer> ids = getChunkSortedIdsByFileMd5(fileMd5);
        removeVideoParts(fileMd5, ids);

        //删除对应的redis数据
        stringRedisTemplate.delete(RedisKey.CHUNKS_MD5.format(fileMd5));
        stringRedisTemplate.opsForZSet().remove(RedisKey.CHUNKS_DEADLINE.format(), fileMd5);
    }

    @Override
    public void removeTempVideoByMd5(String videoMd5) throws Exception {
        minIOUtils.getClient().removeObject(
                RemoveObjectArgs.builder()
                        .bucket(ObjectName.TEMP.getBucket().getBucketName())
                        .object(ObjectName.TEMP.format(videoMd5))
                        .build()
        );
    }

    @Override
    public boolean isChunksMerged(String videoMd5) {
        return Boolean.TRUE.equals(stringRedisTemplate.hasKey(RedisKey.TEMP_VIDEO.format(videoMd5)));
    }

    @Override
    public CheckChunksVO checkChunks(CheckChunksDTO checkChunksDTO) throws Exception {
        //从redis中获取分块上传状态
        Set<Object> ids = getAlreadyUploadChunkIds(checkChunksDTO.getFileMd5());
        Set<Integer> idsSet = ids.stream().map(object -> Integer.valueOf((String) object))
                .collect(Collectors.toSet());

        //为未上传的分块生成临时上传凭证
        int chunkNumber = checkChunksDTO.getChunkNumber();
        List<Map<String, String>> tickets = new ArrayList<>();
        for (int i=1; i<=chunkNumber; i++) {
            if (idsSet.contains(i)) {
                tickets.add(null);
                continue;
            }

            //创建临时上传凭证
            String fileMd5 = checkChunksDTO.getFileMd5();
            PostPolicy policy = new PostPolicy(ObjectName.VIDEO_PART.getBucket().getBucketName(), ZonedDateTime.now().plusMinutes(UploadConst.UPLOAD_CHUNK_TIMEOUT));
            policy.addEqualsCondition("key", ObjectName.VIDEO_PART.format(fileMd5, i));
            policy.addStartsWithCondition("Content-Type", "video/");
            policy.addContentLengthRangeCondition(1L, checkChunksDTO.getChunkSize());
            Map<String, String> formData = minIOUtils.getClient().getPresignedPostFormData(policy);
            formData.put("key", ObjectName.VIDEO_PART.format(fileMd5, i));
            // formData.put("Content-Type", FileUtils.getMimeByExtension(".mp4"));
            tickets.add(formData);
        }

        //返回
        return CheckChunksVO.builder()
                .tickets(tickets)
                .build();
    }


    /**
     * @param fileMd5
     * @return 已经上传的分块的id Set
     */
    private Set<Object> getAlreadyUploadChunkIds(String fileMd5) {
        String key = RedisKey.CHUNKS_MD5.format(fileMd5);
        return stringRedisTemplate.opsForHash().keys(key);
    }

    /**
     *
     * @param fileMd5
     * @return 已经上传的分块的id及对应的md5
     */
    private Map<Object, Object> getAlreadyUploadChunks(String fileMd5) {
        String key = RedisKey.CHUNKS_MD5.format(fileMd5);
        return stringRedisTemplate.opsForHash().entries(key);
    }

    /**
     * 设置某个文件某个分块的md5
     * @param fileMd5
     * @param chunkId
     * @param chunkMd5
     */
    private void setChunkMd5(String fileMd5, String chunkId, String chunkMd5) {
        String key = RedisKey.CHUNKS_MD5.format(fileMd5);
        stringRedisTemplate.opsForHash().putIfAbsent(key, chunkId, chunkMd5);

        //续期24h
        stringRedisTemplate.expire(key, UploadConst.UPLOAD_CHUNK_TIMEOUT, TimeUnit.MINUTES);
        stringRedisTemplate.opsForZSet().add(
                RedisKey.CHUNKS_DEADLINE.format(),
                fileMd5,
                System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(UploadConst.UPLOAD_CHUNK_TIMEOUT)
        );
    }
}
